const LLM_ENDPOINT = process.env.LLM_ENDPOINT;
const LLM_MODEL = process.env.LLM_MODEL;
const LLM_TIMEOUT_MS = process.env.LLM_TIMEOUT_MS;

// 做一个优化，封装一个带超时机制的 fetch 方法

/**
 *
 * @param {*} url 请求的地址，这里对应的是和大模型进行交互的地址
 * @param {*} options fetch 配置项
 * @param {*} timeout 超时时间
 */
async function fetchWithTimeout(url, options = {}, timeout = LLM_TIMEOUT_MS) {
  const controller = new AbortController();

  // 既然都超过 timeout 时间了，那就说明超时
  // 中止请求
  const timeoutId = setTimeout(() => controller.abort(), timeout);
  try {
    const response = await fetch(url, {
      ...options, // 将原本的 fetch 配置项展开
      signal: controller.signal, // 允许中途取消请求
    });

    clearTimeout(timeoutId);

    return response;
  } catch (err) {
    clearTimeout(timeoutId);

    // 超时的错误
    if (err.name === "AbortError") throw new Error("请求超时，请稍候重试");

    // 其它类型的错误：原样抛出
    throw err;
  }
}

/**
 * 调用大模型的接口
 * @param {*} prompt 用户的问题 这是一个数组
 * @param {*} stream 是否是流式
 * @param {*} callback 流式回调函数
 * @returns
 */
async function callLLM({ prompt, stream = false, callback }) {
  const messages = [...prompt];

  const response = await fetchWithTimeout(LLM_ENDPOINT, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.DEEPSEEK_API_KEY}`,
    },
    body: JSON.stringify({
      model: LLM_MODEL,
      messages,
      stream,
    }),
  });

  if (!response.ok)
    throw new Error(
      `模型请求失败☹️：${response.status} : ${response.statusText}`
    );

  // 接下来根据是否是流式，做不同的处理
  if (!stream) {
    // 如果进入此分支，说明采用非流式回复
    const data = await response.json();
    return data.choices?.[0]?.message?.content || "";
  }

  // 代码来到这里，说明采用流式的形式
  const reader = response.body.getReader(); // 拿到 Reader 对象
  const decoder = new TextDecoder("utf-8"); // 创建一个 utf-8 的解码器

  let fullResponse = ""; // 用于存储模型这一次的完整回复

  while (true) {
    const { done, value } = await reader.read(); // 读取当前块的内容
    if (done) break;

    // 对二进制数据进行解码
    const chunk = decoder.decode(value, { stream: true });

    // 后面就是一些 JS 相关的基操了
    const lines = chunk.split("\n").filter((line) => line.trim());

    for (const line of lines) {
      // 针对流式返回的信息，需要根据 deepseek 模型返回的数据的特点，做一些额外的操作
      if (!line.startsWith("data: ")) continue;

      // 移除 "data :" 的前缀
      const jsonStr = line.slice(6);

      // 跳过最后的 [DONE] 标记
      if (jsonStr === "[DONE]") continue;

      try {
        const data = JSON.parse(jsonStr); // data = {"response":"你好"}
        const chunk = data.choices?.[0]?.delta?.content;
        if (chunk) {
          fullResponse += chunk; // 拼接这一次的回答 token 到完整的回复里面
          callback?.(chunk); // 将这一次拿到的 token 回复给回调函数
        }
      } catch (e) {
        console.error("JSON解析失败☹️", e.message);
      }
    }
  }

  return fullResponse;
}

module.exports = {
  /**
   * 非流式回复的接口
   * @param {*} prompt
   * @returns
   */
  callLLM: (prompt) =>
    callLLM({
      prompt,
    }),

  /**
   * 流式回复接口
   * @param {*} prompt
   * @param {*} callback
   * @returns
   */
  callLLMStream: (prompt, callback) =>
    callLLM({
      prompt,
      stream: true,
      callback,
    }),
};
